package Structures;

import wmarduino.BountyHunter;

/**
 * Clase que instancia una nueva estructura grafo con su enlace al primer nodo
 * de la lista de nodos y un entero que representa la cantidad de nodos.
 *
 * @author Jeremy
 * @author Rodrigo
 * @author Grabriel
 * @author Alexandre
 */
public class Domain {

    private SLL _domainBountyHounters; //Lista de cazarecompenzas creados y que se encunetran en el dominio.
    private SLL _travelBountyHounters; //Lista de cazarecompenzas que se encuentran fuera del dominio.
    private static SLL _ltsArduinos;   //Lista de los IPs donde se cencuentra los Arduinos.
    private Node _firstNode; //Referencia al primer nodo de la lista.
    private int _size; //Entero, almacena el tamaño del grafo.
    private int _nextline = 1;//Usado para cambiar de linea en cnsola cuando se estan imprimiendo los identificadores.
    private int _points; // Puntos obtenidos por los bounty Hounters.
    private String _identificator; //Identificador para el grafo (direccion ip del dominio).
    private int countBH; //Identificador de cuantos Bounty Hunters hay en el

    /**
     * Construye un nuevo grafo con su primer nodo en nulo y su tamaño en cero.
     */
    public Domain() {
        _firstNode = null;
        _domainBountyHounters=new SLL();
        _ltsArduinos=new SLL();
        _size = 0;
    }

    //////////////////////////////Getters y setters/////////////////////////////
    
    /**
     * Metodo que regresa la cantididad de Bounty Hunter hay creados en mi domain.
     * @return retorna la cantidad de Bounty Hunters se han creado.
     */
    public int getcountBH(){
        return countBH;
    }
    
    public void setcountBH(){
        countBH ++;
    }
    
    
    /**
     * Obtiene y suma los puntos que lograron recolectar los bounty hunters
     * pertenecientes al dominio.
     *
     * @return puntos obtenidos por los bounty hunters que pertenecen al dominio
     */
    public int getPoints() {
        SLLNode temp =_domainBountyHounters.getFirst();
        while (temp != null){
            _points=_points+((BountyHunter)temp.getElement()).getPoints();
            temp=temp.getSucc();
        }
        SLLNode temp2 =_travelBountyHounters.getFirst();
        while (temp2 != null){
            BountyHunter bount = (BountyHunter) temp2.getElement();
            _points=_points+(bount.getPoints());
            temp2=temp2.getSucc();
        }
        return _points;
    }

    /**
     * Obtiene el primer nodo de la lista enlazada de nodos del grafo.
     *
     * @return Primer nodo del grafo
     */
    public Node getFirstNode() {
        return _firstNode;
    }

    /**
     * Regresa una lista de los identificadores de donde se encuentran los
     * arduinos.
     *
     * @return _lisArduinos Lista de ips arduinos.
     */
    public static SLL getLtsArduinos() {
        return _ltsArduinos;
    }

    /**
     * Establece el primer nodo de la lista de nodos del grafo.
     *
     * @param pNode Nodo de grafo para establecer
     */
    public void setFirstNode(Node pNode) {
        _firstNode = pNode;
    }

    /**
     * Obtiene el tamaño del grafo, cantidad de nodos
     *
     * @return Tamaño del grafo
     */
    public int getsize() {
        return _size;
    }

    /**
     * Obtiene la lista de los cazarecompenzas activos en el dominio
     *
     * @return Lista simple que almacen los caza recompenzas
     */
    public SLL getDomainBountyHounters() {
        return _domainBountyHounters;
    }

    /**
     * Establece la lista simple que almacena los cazarecompenzas del dominio.
     *
     * @param pDomainBountyHounters Lista para establecer
     */
    public void setDomainBountyHounters(SLL pDomainBountyHounters) {
        this._domainBountyHounters = pDomainBountyHounters;
    }

    /**
     * Obtinene la lista simple que almacena los cazarecompenzas que se
     * encuentran activos en otros dominios
     *
     * @return Lista simple
     */
    public SLL getTravelBountyHounters() {
        return _travelBountyHounters;
    }

    /**
     * Establece la lista simple que almacena los cazarecompenzas que estan
     * activos en otros dominios.
     *
     * @param pTravelBountyHounters Lista simple
     */
    public void setTravelBountyHounters(SLL pTravelBountyHounters) {
        this._travelBountyHounters = pTravelBountyHounters;
    }
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Obtiene el grado de salida de un nodo especifico.
     *
     * @param pNode Nodo para obtener su grado de salida
     * @return Entero que representa el grado de salida del nodo dado
     */
    public int getOutDegree(Node pNode) {
        return pNode.getOutDegree();
    }

    /**
     * Retorna el grado completo de un nodo, tanto el de salida como el de
     * entrada sumados.
     *
     * @param pNode Nodo para obtener su grado
     * @return Entero que representa el grado del nodo dado
     */
    public int getDegree(Node pNode) {
        int count = 0;

        for (Node n = this._firstNode; n != null; n = n.getNextNode()) {

            Edge edge = n.getFirstOutEdge();
            while (edge != null) {
                if (edge.getSource() == pNode || edge.getDest() == pNode) {
                    count += 1;
                    edge = edge.getNextOutEdge();
                } else {
                    edge = edge.getNextOutEdge();
                }
            }
        }
        return count;
    }

    /**
     * Obtiene el identificador del grafo(direccion ip del dominio).
     *
     * @return Identificador
     */
    public String getIdentificator() {
        return _identificator;
    }

    /**
     * Establece el identificador del grafo o direccion ip del dominio
     *
     * @param pIdentificador Identificador para establecer
     */
    public void setIdentificator(String pIdentificador) {
        this._identificator = pIdentificador;
    }

    /**
     * Busca un nodo de acuerdo a la posicion representada por el entero del
     * parametro y lo retorna, las posiciones de los nodos comienzan en cero.
     *
     * @param pPos Posicion del nodo
     * @return Nodo en esa posicion
     */
    public Node findNode(int pPos) {
        Node tmp = _firstNode;
        for (int i = 0; i < _size; i++) {
            if (i == pPos) {
                return tmp;
            } else {
                tmp = tmp.getNextNode();
            }
        }
     //  System.out.println("La posicion es mayor al tamaño del grafo");
        return null;
    }

    /**
     * Retorna un buleano que representa si hay camino entre dos nodos, de
     * pNode1 a pNode2
     *
     * @param pNode1 Nodo fuente, donde inicia el camino
     * @param pNode2 Nodo destino, donde termina el camino
     * @return true si hay camino entre los nodos, false de lo contrario
     */
    public boolean containEdge(Node pNode1, Node pNode2) {
        Node source = (Node) pNode1,
                dest = (Node) pNode2;

        for (Edge edge = source.getFirstOutEdge(); edge != null; edge = edge.getNextOutEdge()) {
            if (edge.getDest() == dest) {
                return true;
            }
        }
        return false;
    }

    /**
     * Retorna un buleano que representa si hay camino entre dos nodos, de
     * pNode1 a pNode2
     *
     * @param pNode1 Nodo fuente, donde inicia el camino
     * @param pNode2 Nodo destino, donde termina el camino
     * @return true si hay camino entre los nodos, false de lo contrario
     */
    public boolean removedEdgeGraph(Node pNode1, Node pNode2) {
        Node source = (Node) pNode1,
                dest = (Node) pNode2;

        for (Edge edge = source.getFirstOutEdge(); edge != null; edge = edge.getNextOutEdge()) {
            if (edge.getDest() == dest) {
                removeEdge(edge);
                return true;
            }
        }
        return false;
    }

    /**
     * Borra todos los elementos del grafo.
     */
    public void clear() {

        for (Node n = _firstNode; n != null; n = n.getNextNode()) {
            n.setFirstOutEdge(null);
        }
        _firstNode = null;
        _size = 0;
    }

    /**
     * Añade un nodo al grafo dirigido.
     *
     * @param pElement Dato para el nodo nuevo.
     */
    public void addNode(Object pElement) {
        Node ins = new Node(pElement);

        if (_firstNode == null) {
            _firstNode = ins;
        } else {
            Node tmp = _firstNode;

            while (tmp.getNextNode() != null) {
                tmp = tmp.getNextNode();
            }
            tmp.setNextNode(ins);
            ins.setPrevNode(tmp);
        }
        _size += 1;
    }

    /**
     * Añade una arista o camino que comunica a un nodo fuente(pNodeSource) y a
     * un nodo destino(pNodeDest), y sin un peso para la misma.
     *
     * @param pNodeSource Nodo fuente, donde inicia el camino
     * @param pNodeDest Nodo destino, donde termina el camino
     * @return Arista nueva
     */
    public Edge addEdge(Node pNodeSource, Node pNodeDest) {
        Edge ins = new Edge(pNodeSource, pNodeDest);

        if (pNodeSource.getFirstOutEdge() == null) {
            pNodeSource.setFirstOutEdge(ins);
        } else {
            Edge tmp = pNodeSource.getFirstOutEdge();
            while (tmp.getNextOutEdge() != null) {
                tmp = tmp.getNextOutEdge();
            }
            tmp.setNextOutEdge(ins);
        }
        pNodeSource.setOutDegree(pNodeSource.getOutDegree() + 1);
        return ins;
    }

    /**
     * Añade una arista o camino que comunica a un nodo fuente(pNodeSource) y a
     * un nodo destino(pNodeDest), y un peso para la misma.
     *
     * @param pNodeSource Nodo donde inicia el camino
     * @param pNodeDest Nodo donde finaliza el camino
     * @param pAttr Atributo o peso para la arista
     * @return Arista nueva ya creada
     */
    public Edge addEdge(Node pNodeSource, Node pNodeDest, int pAttr) {
        Edge ins = new Edge(pNodeSource, pNodeDest, pAttr);

        if (containEdge(pNodeSource, pNodeDest)) {
            return null;
        } else {
            if (pNodeSource.getFirstOutEdge() == null) {
                pNodeSource.setFirstOutEdge(ins);
            } else {
                Edge tmp = pNodeSource.getFirstOutEdge();
                while (tmp.getNextOutEdge() != null) {
                    tmp = tmp.getNextOutEdge();
                }
                tmp.setNextOutEdge(ins);
            }
            pNodeSource.setOutDegree(pNodeSource.getOutDegree() + 1);
            return ins;
        }
    }

    /**
     * Remueve un Nodo, dado como parametro, del grafo.
     *
     * @param pNode Nodo para eliminar
     */
    public void removeNode(Node pNode) {

        if (pNode == _firstNode) {
            removeAdyEdges(this, pNode);
            
            _firstNode.setFirstOutEdge(null);
            _firstNode.getNextNode().setPrevNode(null);
            _firstNode = _firstNode.getNextNode();
            
            _size -= 1;
        } else {
            if (pNode.getNextNode() == null) {
                removeAdyEdges(this, pNode);
                
                pNode.setFirstOutEdge(null);
                pNode.getPrevNode().setNextNode(pNode.getNextNode());
                
                _size -= 1;
            } else {
                removeAdyEdges(this, pNode);
                
                pNode.setFirstOutEdge(null);
                pNode.getPrevNode().setNextNode(pNode.getNextNode());
                pNode.getNextNode().setPrevNode(pNode.getPrevNode());
                
                _size -= 1;
            }
        }
    }

    /**
     * Remueve un camino, dado como parametro, del grafo.
     *
     * @param pEdge Camino para eliminar
     */
    public void removeEdge(Edge pEdge) {
        System.out.println(pEdge.getSource().getElement());
        if (pEdge == pEdge.getSource().getFirstOutEdge()) {
            pEdge.getSource().setFirstOutEdge(pEdge.getNextOutEdge());
        } else {
            Edge tmp = pEdge.getSource().getFirstOutEdge();
            while (tmp.getNextOutEdge() != pEdge) {
                tmp = tmp.getNextOutEdge();
            }
            tmp.setNextOutEdge(pEdge.getNextOutEdge());
            tmp.getSource().setOutDegree(tmp.getSource().getOutDegree() - 1);
        }
    }

    /**
     * Realiza el recorrido por profundidad del grafo y lo muestra en un
     * componente grafico dado como parametro.
     *
     * @param pNode Nodo por el cual iniciar el recorrido
     */
    public void depthTour(Node pNode) {
        _nextline = 1;
        UnBoundedStack stack = new UnBoundedStack();
        if (pNode != null) {
            stack.top(pNode);

            while (!stack.isEmpty()) {
                Node nodeVisited = stack.getTop();
                if (nodeVisited.isTour() != true) {
                    nodeVisited.setTour(true);
                    if (_nextline == 10) {
                        _nextline = 1;
                        System.out.print(" [" + nodeVisited.getElement() + "]");
                        System.out.println("");
                    } else {
                        _nextline++;
                        System.out.print(" [" + nodeVisited.getElement() + "]");
                    }

                    fillStack(nodeVisited, stack);
                } else {
                    fillStack(nodeVisited, stack);
                }
            }
            System.out.print("\n");
            changeTourNode();
        }
    }

    /**
     * Realiza el recorrido por anchura del grafo y lo muestra en un componente
     * grafico dado como parametro.
     *
     * @param pNode Nodo por el cual iniciar el recorrido
     *
     */
    public void breadthTour(Node pNode) {
        if (pNode != null) {
            Queue queue = new Queue();
            queue.enqueue(pNode);
            System.out.print(" [" + pNode.getElement() + "]");
            pNode.setTour(true);

            while (!queue.isEmpty()) {
                Node nod = queue.getFirst();
                tourAdyNodes(nod, queue);
            }
            System.out.print("\n");
            changeTourNode();
        }
    }

    /**
     * Imprime en consola el identificador de cada nodo enlazado a la lista de
     * nodos del grafo.
     */
    public void printNodes() {
        _nextline = 1;
        for (Node n = _firstNode; n != null; n = n.getNextNode()) {
            if (_nextline == 10) {
                _nextline = 1;
                System.out.print(" [" + n.getElement() + "]");
                System.out.println("");
            } else {
                _nextline++;
                System.out.print(" [" + n.getElement() + "]");
            }
        }
    }

    /**
     * Imprime un recorrido completo de las conexiones junto con sus pesos.
     */
    public void conectionsTour() {
        int i = 1;
        for (Node n = _firstNode; n != null; n = n.getNextNode()) {
            for (Edge e = n.getFirstOutEdge(); e != null; e = e.getNextOutEdge()) {
                System.out.println(" " + i + ")." + " [" + n.getElement() + "]" + " --> " + e.getAttribute() + " --> " + "[" + e.getDest().getElement() + "]");
                i++;
            }
        }
    }

//    public Node getNearestPizzeria(Node house) {
//        //Metodo para obteneer la pizzeria mas cercanan a una determinada casa.
//        double recorrido = Integer.MAX_VALUE;
//        Node piz = null;
//
//        for (Node n = _firstNode; n != null; n = n.nextNode) {
//            if (n.identifier == true) {
//                double dis = dijkstra(house, n);
//
//                if (dis < recorrido) {
//                    recorrido = dis;
//                    piz = n;
//                }
//            }
//        }
//        return piz;
//    }
    /**
     * Algoritmo de dijkstra modificado para obtener la ruta mas corta de un
     * nodo hacia uno especifico.
     *
     * @param pHouse Nodo sobre el cual iniciar el algoritmo
     * @param pDest Nodo al que se requiere encontrar la ruta mas corta
     * @return La distancia total mas corta hacia el nodo requerido
     */
    public int dijkstra(Node pHouse, Node pDest) {
        PriorityQueue cola = new PriorityQueue();//Alamacena los vertices que quedan por recorrer y verificar.
        SLL finalList = new SLL();//Almacena la lista de nodos con sus distancias finales.
        SLL adyList = new SLL();//Almacena la lista de nodos adyascentes al que esta siendo comprobado.

        cola.add(pHouse);
        while (!cola.isEmpty()) {
            Node tmp = cola.dequeue();
            finalList.insert(tmp);
            tmp.setTour(true);
            adyList.clear();
            searchAdyNodes(adyList, tmp);
            for (SLLNode n = adyList.getFirst(); n != null; n = n.getSucc()) {
                int dist = calcDist(n, tmp);
                Node nNode = (Node) n.getElement();
                if (nNode.isInQueue(cola) == false) {
                    nNode.setDistance(dist);
                    cola.add(nNode);
                } else {
                    updateDistances(cola, nNode, dist);
                }
            }
        }
        int NodeDistance = searchNode(finalList, pDest);
        changeDistances();
        changeTourNode();
        return NodeDistance;
    }

    ///////////////////////Metodos y funciones auxiliares///////////////////////
    /**
     * Metodo auxiliar para el recorrido por anchura para recorrer los nodos
     * adyascentes al marcado y añadirlos a la cola e imprimirlos.
     *
     * @param pNode Nodo al cual revisar sus adyascentes
     * @param pQueue Cola para añadir los nodos adyascentes
     */
    private void tourAdyNodes(Node pNode, Queue pQueue) {
        _nextline = 2;
        for (Node n = _firstNode; n != null; n = n.getNextNode()) {
            for (Edge e = n.getFirstOutEdge(); e != null; e = e.getNextOutEdge()) {
                if ((e.getSource().getElement().equals(pNode.getElement()))
                        && (e.getDest().isTour() != true)) {
                    if (_nextline == 10) {
                        _nextline = 1;
                        System.out.print(" [" + e.getDest().getElement() + "]");
                        System.out.println("");
                    } else {
                        _nextline++;
                        System.out.print(" [" + e.getDest().getElement() + "]");
                    }
                    pQueue.enqueue(e.getDest());
                    e.getDest().setTour(true);
                } else if ((e.getDest().getElement().equals(pNode.getElement()))
                        && (e.getSource().isTour() != true)) {
                    if (_nextline == 10) {
                        _nextline = 1;
                        System.out.print(" [" + e.getSource().getElement() + "]");
                        System.out.println("");
                    } else {
                        _nextline++;
                        System.out.print(" [" + e.getSource().getElement() + "]");
                    }
                    pQueue.enqueue(e.getSource());
                    e.getSource().setTour(true);

                }
            }
        }
        pQueue.dequeue();
    }

    /**
     * Metodo auxiliar del metodo dijkstra, usado para agregar a una lista
     * (pAdyList) los nodos adyascentes al visitado (pTmp).
     *
     * @param pAdyList Lista para agregar los nodos adyascentes al visitado
     * @param pTmp Nodo que esta siendo visitado actualmente
     */
    private void searchAdyNodes(SLL pAdyList, Node pTmp) {
        for (Node n = _firstNode; n != null; n = n.getNextNode()) {
            for (Edge e = n.getFirstOutEdge(); e != null; e = e.getNextOutEdge()) {
                if ((e.getSource().getElement().equals(pTmp.getElement()))
                        && (e.getDest().isTour() != true)) {
                    pAdyList.insert(e.getDest());
                }
//                else if ((e.getDest().getElement().equals(pTmp.getElement()))
//                        && (e.getSource().isTour() != true)) {
//                    pAdyList.insert(e.getSource());
//                }
            }
        }
    }

    /**
     * Funcion auxiliar para dijkstra que calcula la distancia del nodo visitado
     * hasta la fuente.
     *
     * @param pNode Nodo fuente
     * @param pTmp Nodo destino
     * @return
     */
    private int calcDist(SLLNode pNode, Node pTmp) {
        int dis = 0;
        Node nNode = (Node)pNode.getElement();
        for (Node n = _firstNode; n != null; n = n.getNextNode()) {
            for (Edge e = n.getFirstOutEdge(); e != null; e = e.getNextOutEdge()) {
                
                if (((e.getSource().getElement().equals(nNode.getElement())
                        && (e.getDest().getElement().equals(pTmp.getElement())))
                        || ((e.getSource().getElement().equals(pTmp.getElement()))
                        && (e.getDest().getElement().equals(nNode.getElement()))))) {
                    dis = e.getAttribute() + pTmp.getDistance();
                }
            }
        }
        return dis;
    }

    /**
     * Metodo auxiliar del dijkstra que acualiza la distancia del nodo visitado
     * por la menor.
     *
     * @param pQueue Cola para buscar el nodo para actualizar
     * @param pNode Nodo para buscar
     * @param pDist Distancia actual para comparar
     */
    private void updateDistances(PriorityQueue pQueue, Node pNode, int pDist) {
        for (SLLNode n = pQueue.getFirst(); n != null; n = n.getSucc()) {
            Node nNode = (Node) n.getElement();
            if (nNode.getElement().equals(pNode.getElement())) {
                if (nNode.getDistance() < pDist) {
                    nNode.setDistance(pDist);
                    return;
                }
            }
        }
    }

    /**
     * Metodo auxiliar al dijkstra que busca el nodo que se quiere como mas
     * cercano y retorna su distancia.
     *
     * @param pList Lista de nodos para buscar
     * @param pNode Nodo para buscar
     * @return Distancia del nodo encntrado
     */
    private int searchNode(SLL pList, Node pNode) {
        for (SLLNode n = pList.getFirst(); n != null; n = n.getSucc()) {
            Node nNode = (Node) n.getElement();
            if (nNode.getElement().equals(pNode.getElement())) {
                return pNode.getDistance();
            }
        }
        return 0;
    }

    /**
     * Metodo auxiliar al dijkstra que resetea las distancias de cada nodo a
     * cero para poder aplicar dijkstra en otra ocasion.
     */
    private void changeDistances() {
        for (Node n = _firstNode; n != null; n = n.getNextNode()) {
            n.setDistance(0);
        }
    }

    /**
     * Metodo auxiliar de los recorridos que cambia el atributo de cada nodo,
     * para verificar si ya ha sido recorrido, a false.
     */
    private void changeTourNode() {
        for (Node n = this._firstNode; n != null; n = n.getNextNode()) {
            n.setTour(false);
        }
    }

    /**
     * Metodo auxiliar para el recorrido que llena la pila con algun nodo
     * adyescente que no haya sido recorrido.
     *
     * @param pNodeVisited Nodo que esta siendo visitado actualmente
     * @param pStack Pila para los nodos adyascentes
     */
    private void fillStack(Node pNodeVisited, UnBoundedStack pStack) {
        for (Node n = this._firstNode; n != null; n = n.getNextNode()) {

            for (Edge e = n.getFirstOutEdge(); e != null; e = e.getNextOutEdge()) {

                if (e.getDest() == pNodeVisited && e.getSource().isTour() == false) {
                    pStack.top(e.getSource());
                    return;
                } else if (e.getSource() == pNodeVisited && e.getDest().isTour() == false) {
                    pStack.top(e.getDest());
                    return;
                }
            }
        }
        pStack.pop();
    }

    /**
     * Metodo auxiliar del metodo removeNode para remover las aristas que
     * conducen a los nodos adyacentes al eliminado.
     *
     * @param pGraph Grafo sobre el cual ejecutar el metodo
     * @param pNode Nodo para eliminar
     */
    private void removeAdyEdges(Domain pGraph, Node pNode) {
        for (Node n = pGraph._firstNode; n != null; n = n.getNextNode()) {
            Edge e = n.getFirstOutEdge();
            while (e != null) {
                if (e.getDest() == pNode) {
                    removeEdge(e);
                    n.setOutDegree(n.getOutDegree() - 1);
                    e = e.getNextOutEdge();
                } else {
                    e = e.getNextOutEdge();
                }
            }
        }
    }
    
    
    
}
